package ualrcdg.shared;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.nio.file.Path;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.xerces.parsers.DOMParser;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import ualrcdg.shared.exceptions.CDGException;

/**
 * Utility class that encapsulates all generic DOM/XML parsing and writing methods.
 */
public class XMLHelper
{
	/* LATER potential efficiency improvements:
	 * Check to see if DOMParser.reset method resets features that have been set (validation, include-ignorable-whitespace), or the error handler, or if it deletes the parsed document.
	 * If none of the above, create a single, static DOMParser and reset it after each use 
	 * 
	 * Consider a static transformer
	 * 
	 * Test which of the two toString methods below is faster (and/or less resource-intensive). Discard the other
	 *  - documentToString( doc )
	 *  - writeDocumentToString( doc )
	 */
	
	protected static final TransformerFactory transformerFactory = TransformerFactory.newInstance( );
	
	{
		transformerFactory.setAttribute( "{http://xml.apache.org/xslt}indent-number", 5 );
	}
	
	public static final String ERROR_XMLWRITE_FAILED = "#ERROR_XMLWRITE_FAILED#";
	
	protected static DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance( );
	protected static DocumentBuilder builder = null;
	
	protected static CDGEntityResolver dtdResolver  = null;
	
	static boolean initialized = false;
	
	/**
	 * Static initializer method for this class's helper members
	 * 
	 * @param rootPath the base path that will be used to initialize the associated {@link CDGEntityResolver} instance
	 * @param dtdListingFile the configuration file used to initialize the associated {@link CDGEntityResolver} instance
	 * @throws FileNotFoundException If the specified configuration file cannot be located
	 * @throws IOException If an IO error occurs during the CDGEntityResolver instance's initialization 
	 * @throws ParserConfigurationException If an error occurs during the associated DocumentBuilder instance's initialization  
	 * @throws CDGException If an error occurs during the initialization of the CDGEntityResolver instance
	 */
	public static void init( Path rootPath, String dtdListingFile ) throws FileNotFoundException, IOException, ParserConfigurationException, CDGException 
	{
		if( initialized )
		{
			return;
		}
		
		System.out.println( "CDGXMLHelper.init(" + rootPath + ", " + dtdListingFile + ")" );
		
		dtdResolver = new CDGEntityResolver( rootPath, dtdListingFile );
		
		builder = documentBuilderFactory.newDocumentBuilder();
		
		initialized = true;
	}
	
	/**
	 * Converts a given DOM object into a simple XML String form
	 * 
	 * @param doc the DOM object to be converted
	 * @return the String representation of the given doc
	 */
	//LATER check speed of this method against writeDocumentToString, use the faster/more effiecient 
	@SuppressWarnings("unused")
	private static String documentToString( Document doc )
	{
		//via http://stackoverflow.com/questions/508428/is-there-a-simple-way-to-convert-my-xml-object-back-to-string-in-java
		
		DOMSource dom = new DOMSource( doc );
		StringWriter writer = new StringWriter( );
		StreamResult result = new StreamResult( writer );

		try
		{
			Transformer transformer = transformerFactory.newTransformer( );
			transformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "yes" );
		
			DocumentType docType = doc.getDoctype( );
			
			if( docType != null )
			{
				transformer.setOutputProperty( OutputKeys.DOCTYPE_SYSTEM, docType.getSystemId( ) );
			}
			transformer.transform( dom, result );
			return writer.toString( );
		}
		catch( TransformerException e )
		{
			return ERROR_XMLWRITE_FAILED;
		}
	}

	/**
	 * Parses a DOM object from an XML file
	 * 
	 * @param xmlFile the file to be parsed
	 * @return the parsed DOM object
	 * @throws SAXException If the file cannot be parsed 
	 * @throws IOException If an IO error occurs during the parsing of the XML file
	 */
	public static Document parseDocumentFromFile( File xmlFile ) throws SAXException, IOException
	{
		//via http://stackoverflow.com/questions/15732/whats-the-best-way-to-validate-an-xml-file-against-an-xsd-file
		
		DOMParser parser = new DOMParser( );
		parser.setFeature( "http://xml.org/sax/features/validation", true );
		parser.setFeature( "http://apache.org/xml/features/dom/include-ignorable-whitespace", false );
		parser.setErrorHandler( CDGParserErrorHandler.getInstance( ) );
		parser.setEntityResolver( dtdResolver );
		
		FileReader reader = new FileReader( xmlFile );
		
		parser.parse( new InputSource( reader ) );
		
		reader.close( );
		
		return parser.getDocument( );
	}
	
	/**
	 * Parses a DOM object from an InputStream containing XML markup
	 * 
	 * @param in the stream from which the DOM object should be parsed
	 * @return the parsed DOM object
	 * @throws SAXException If no DOM object can be parsed from the stream
	 * @throws IOException If an IO error occurs during the parsing of the stream's contents
	 */
	public static Document parseDocumentFromStream( InputStream in ) throws SAXException, IOException
	{
		//via http://stackoverflow.com/questions/15732/whats-the-best-way-to-validate-an-xml-file-against-an-xsd-file
		
		DOMParser parser = new DOMParser( );
		parser.setFeature( "http://xml.org/sax/features/validation", true );
		parser.setFeature( "http://apache.org/xml/features/dom/include-ignorable-whitespace", false );
		parser.setErrorHandler( CDGParserErrorHandler.getInstance( ) );
		parser.setEntityResolver( dtdResolver );
		
		parser.parse( new InputSource( in ) );
		
		return parser.getDocument( );
	}
	
	/**
	 * Writes a DOM object to a given OutputStream
	 * 
	 * @param doc the DOM object to be written
	 * @param out the stream to which the DOM object should be written
	 * @throws TransformerException If the DOM object cannot be written to the stream
	 * @throws IOException If an IO error occurs while writing the DOM object to the stream
	 */
	private static void writeDocumentToStream( Document doc, OutputStream out ) throws TransformerException, IOException
	{
		//via http://stackoverflow.com/questions/8257748/serialize-dom-to-fileoutputstream-using-xerces
		Transformer idTransform = transformerFactory.newTransformer( );
		idTransform.setOutputProperty( OutputKeys.METHOD, "xml" );
		idTransform.setOutputProperty( OutputKeys.INDENT, "yes" );
		idTransform.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "yes" );
		
		DocumentType docType = doc.getDoctype( );
		
		if( docType != null )
		{
			idTransform.setOutputProperty( OutputKeys.DOCTYPE_SYSTEM, docType.getSystemId( ) );
		}
		
		Source input = new DOMSource( doc );
		Result output = new StreamResult( out );
		idTransform.transform( input, output );
		
		out.flush( );
	}
	
	/**
	 * Writes a DOM object to a file with the given file name
	 * 
	 * @param doc the DOM object to be written
	 * @param xmlFileName the name of the XML output file
	 * @throws FileNotFoundException If the output file cannot be created
	 * @throws TransformerException If the DOM object cannot be written to the file
	 * @throws IOException If an IO error occurs during the writing of the output file
	 */
	public static void writeDocumentToFile( Document doc, String xmlFileName ) throws FileNotFoundException, TransformerException, IOException
	{
		FileOutputStream out = new FileOutputStream( new File( xmlFileName ) );
		
		writeDocumentToStream( doc, out );
		
		out.close( );
	}
	
	/**
	 * Converts a given DOM object into a simple XML String form
	 * 
	 * @param doc the DOM object to be converted
	 * @return the String representation of the given doc
	 * @throws TransformerException If the DOM object cannot be converted to a String
	 * @throws IOException If an IO error occurs during the converting of the DOM object
	 */
	public static byte[] writeDocumentToBytes( Document doc ) throws IOException, TransformerException 
	{
		ByteArrayOutputStream baos = new ByteArrayOutputStream( );
		writeDocumentToStream( doc, baos );

		baos.flush( );

		byte[ ] result = baos.toByteArray( );

		baos.close( );

		return result;
	}

	/**
	 * Parses a DOM object from a byte array containing XML markup
	 * 
	 * @param doc the byte array from which a DOM object should be parsed
	 * @return the parsed DOM object
	 * @throws TransformerException If no DOM object can be parsed from the byte array
	 * @throws IOException If an IO error occurs during the parsing of the stream's contents
	 * @throws SAXException If no DOM object can be parsed from the byte array
	 */
	public static Document parseDocumentFromBytes( byte[] doc ) throws TransformerException, IOException, SAXException
	{
		ByteArrayInputStream bais = new ByteArrayInputStream( doc );
		
		Document result = parseDocumentFromStream( bais );
		
		bais.close( );
		
		return result;
	}
	
	/**
	 * Convenience method for copying one stream to another
	 * 
	 * @param is the source to be copied from
	 * @param os the destination to be written to
	 * @throws IOException If an IO error occurs while the input is being copied to the output
	 */
	public static void copyStream( InputStream is, OutputStream os ) throws IOException
	{
		byte[] buffer = new byte[8192];
		
		int readCount = 0;
		
		while( ( readCount = is.read( buffer ) ) != -1 )
		{
			os.write( buffer, 0, readCount );
		}
		
		os.flush( );
	}
}
